L.Map.mergeOptions({
    touchExtend: true
});

/**
 * @class L.Map.TouchExtend
 * @aka TouchExtend
 */
L.Map.TouchExtend = L.Handler.extend({

    // @method initialize(): void
    // Sets TouchExtend private accessor variables
    initialize: function (map) {
        this._map = map;
        this._container = map._container;
        this._pane = map._panes.overlayPane;
    },

    // @method addHooks(): void
    // Adds dom listener events to the map container
    addHooks: function () {
        L.DomEvent.on(this._container, 'touchstart', this._onTouchStart, this);
        L.DomEvent.on(this._container, 'touchend', this._onTouchEnd, this);
        L.DomEvent.on(this._container, 'touchmove', this._onTouchMove, this);
        if (this._detectIE()) {
            L.DomEvent.on(this._container, 'MSPointerDown', this._onTouchStart, this);
            L.DomEvent.on(this._container, 'MSPointerUp', this._onTouchEnd, this);
            L.DomEvent.on(this._container, 'MSPointerMove', this._onTouchMove, this);
            L.DomEvent.on(this._container, 'MSPointerCancel', this._onTouchCancel, this);

        } else {
            L.DomEvent.on(this._container, 'touchcancel', this._onTouchCancel, this);
            L.DomEvent.on(this._container, 'touchleave', this._onTouchLeave, this);
        }
    },

    // @method removeHooks(): void
    // Removes dom listener events from the map container
    removeHooks: function () {
        L.DomEvent.off(this._container, 'touchstart', this._onTouchStart);
        L.DomEvent.off(this._container, 'touchend', this._onTouchEnd);
        L.DomEvent.off(this._container, 'touchmove', this._onTouchMove);
        if (this._detectIE()) {
            L.DomEvent.off(this._container, 'MSPointerDowm', this._onTouchStart);
            L.DomEvent.off(this._container, 'MSPointerUp', this._onTouchEnd);
            L.DomEvent.off(this._container, 'MSPointerMove', this._onTouchMove);
            L.DomEvent.off(this._container, 'MSPointerCancel', this._onTouchCancel);
        } else {
            L.DomEvent.off(this._container, 'touchcancel', this._onTouchCancel);
            L.DomEvent.off(this._container, 'touchleave', this._onTouchLeave);
        }
    },

    _touchEvent: function (e, type) {
        // #TODO: fix the pageX error that is do a bug in Android where a single touch triggers two click events
        // _filterClick is what leaflet uses as a workaround.
        // This is a problem with more things than just android. Another problem is touchEnd has no touches in
        // its touch list.
        var touchEvent = {};
        if (typeof e.touches !== 'undefined') {
            if (!e.touches.length) {
                return;
            }
            touchEvent = e.touches[0];
        } else if (e.pointerType === 'touch') {
            touchEvent = e;
            if (!this._filterClick(e)) {
                return;
            }
        } else {
            return;
        }

        var containerPoint = this._map.mouseEventToContainerPoint(touchEvent),
            layerPoint = this._map.mouseEventToLayerPoint(touchEvent),
            latlng = this._map.layerPointToLatLng(layerPoint);

        this._map.fire(type, {
            latlng: latlng,
            layerPoint: layerPoint,
            containerPoint: containerPoint,
            pageX: touchEvent.pageX,
            pageY: touchEvent.pageY,
            originalEvent: e
        });
    },

    /** Borrowed from Leaflet and modified for bool ops **/
    _filterClick: function (e) {
        var timeStamp = (e.timeStamp || e.originalEvent.timeStamp),
            elapsed = L.DomEvent._lastClick && (timeStamp - L.DomEvent._lastClick);

        // are they closer together than 500ms yet more than 100ms?
        // Android typically triggers them ~300ms apart while multiple listeners
        // on the same event should be triggered far faster;
        // or check if click is simulated on the element, and if it is, reject any non-simulated events
        if ((elapsed && elapsed > 100 && elapsed < 500) || (e.target._simulatedClick && !e._simulated)) {
            L.DomEvent.stop(e);
            return false;
        }
        L.DomEvent._lastClick = timeStamp;
        return true;
    },

    _onTouchStart: function (e) {
        if (!this._map._loaded) {
            return;
        }

        var type = 'touchstart';
        this._touchEvent(e, type);

    },

    _onTouchEnd: function (e) {
        if (!this._map._loaded) {
            return;
        }

        var type = 'touchend';
        this._touchEvent(e, type);
    },

    _onTouchCancel: function (e) {
        if (!this._map._loaded) {
            return;
        }

        var type = 'touchcancel';
        if (this._detectIE()) {
            type = 'pointercancel';
        }
        this._touchEvent(e, type);
    },

    _onTouchLeave: function (e) {
        if (!this._map._loaded) {
            return;
        }

        var type = 'touchleave';
        this._touchEvent(e, type);
    },

    _onTouchMove: function (e) {
        if (!this._map._loaded) {
            return;
        }

        var type = 'touchmove';
        this._touchEvent(e, type);
    },

    _detectIE: function () {
        var ua = window.navigator.userAgent;

        var msie = ua.indexOf('MSIE ');
        if (msie > 0) {
            // IE 10 or older => return version number
            return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
        }

        var trident = ua.indexOf('Trident/');
        if (trident > 0) {
            // IE 11 => return version number
            var rv = ua.indexOf('rv:');
            return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
        }

        var edge = ua.indexOf('Edge/');
        if (edge > 0) {
            // IE 12 => return version number
            return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10);
        }

        // other browser
        return false;
    }
});

L.Map.addInitHook('addHandler', 'touchExtend', L.Map.TouchExtend);


/**
 * @class L.Marker.Touch
 * @aka Marker.Touch
 *
 * This isn't full Touch support. This is just to get markers to also support dom touch events after creation
 * #TODO: find a better way of getting markers to support touch.
 */
L.Marker.Touch = L.Marker.extend({

    _initInteraction: function () {
        if (!this.addInteractiveTarget) {
            // 0.7.x support
            return this._initInteractionLegacy();
        }
        // TODO this may need be updated to re-add touch events for 1.0+
        return L.Marker.prototype._initInteraction.apply(this);
    },

    // This is an exact copy of https://github.com/Leaflet/Leaflet/blob/v0.7/src/layer/marker/Marker.js
    // with the addition of the touch events
    _initInteractionLegacy: function () {

        if (!this.options.clickable) {
            return;
        }

        // TODO refactor into something shared with Map/Path/etc. to DRY it up

        var icon = this._icon,
            events = ['dblclick',
                'mousedown',
                'mouseover',
                'mouseout',
                'contextmenu',
                'touchstart',
                'touchend',
                'touchmove'];
        if (this._detectIE) {
            events.concat(['MSPointerDown',
                'MSPointerUp',
                'MSPointerMove',
                'MSPointerCancel']);
        } else {
            events.concat(['touchcancel']);
        }

        L.DomUtil.addClass(icon, 'leaflet-clickable');
        L.DomEvent.on(icon, 'click', this._onMouseClick, this);
        L.DomEvent.on(icon, 'keypress', this._onKeyPress, this);

        for (var i = 0; i < events.length; i++) {
            L.DomEvent.on(icon, events[i], this._fireMouseEvent, this);
        }

        if (L.Handler.MarkerDrag) {
            this.dragging = new L.Handler.MarkerDrag(this);

            if (this.options.draggable) {
                this.dragging.enable();
            }
        }
    },

    _detectIE: function () {
        var ua = window.navigator.userAgent;

        var msie = ua.indexOf('MSIE ');
        if (msie > 0) {
            // IE 10 or older => return version number
            return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
        }

        var trident = ua.indexOf('Trident/');
        if (trident > 0) {
            // IE 11 => return version number
            var rv = ua.indexOf('rv:');
            return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
        }

        var edge = ua.indexOf('Edge/');
        if (edge > 0) {
            // IE 12 => return version number
            return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10);
        }

        // other browser
        return false;
    }
});